package co.edu.aes.patrones.dispatcher.receptor;

import co.edu.aes.patrones.forwardreceiver.almacen.Almacen;
import co.edu.aes.patrones.forwardreceiver.almacen.UbicacionFisica;
import co.edu.aes.patrones.forwardreceiver.configuracion.ConstantesConfiguracion;
import co.edu.aes.patrones.forwardreceiver.mensajes.MensajeTCP;
import co.edu.aes.patrones.forwardreceiver.mensajes.MensajeUDP;
import co.edu.aes.patrones.forwardreceiver.receptor.ReceptorTCP;
import co.edu.aes.patrones.forwardreceiver.receptor.ReceptorUDP;
import co.edu.aes.patrones.forwardreceiver.transmisor.Transmisor;
import co.edu.aes.patrones.forwardreceiver.utilidades.ComunicacionException;
import co.edu.aes.patrones.forwardreceiver.utilidades.TipoComunicacion;
import co.edu.aes.patrones.forwardreceiver.utilidades.TiposEncabezados;
import co.edu.aes.patrones.forwardreceiver.utilidades.Utilidades;
import java.io.IOException;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Hilo Receptor del Despachador el cual tiene la función de estar pendiente de mensajes entrantes y
 * realizar alguna acción correspondiente.
 *
 * @author Alejandro León
 * @author Alejandro Peña
 * @author Diego Escobar.
 */
public class Receptor implements Runnable {

    private static final int MAXIMO_DE_CONEXIONES = 10;
    private TipoComunicacion tipoComunicacion;
    /**
     * Punto de comunicación entrante TCP.
     */
    private ServerSocket socketServidorTCP;
    /**
     * Conexión entrante.
     */
    private Socket socketClienteTCP;
    /**
     * Punto de comunicación entrante UDP.
     */
    private MulticastSocket socketMultiCast;
    private InetAddress grupo;

    /**
     * Método encargado de cerrar la comunicación UDP.
     *
     * @throws Exception
     */
    public void cerrarConexion() throws IOException {
        socketMultiCast.leaveGroup(grupo);
        socketMultiCast.close();
    }

    /**
     * Inicio de configuración del Receptor según el tipo de comunicación a utilizar.
     *
     * @param puerto Puerto donde se encontrará el receptor escuchando.
     * @param tipoComunicacion El tipo de comunicación a configurar TCP o UDP.
     * @throws ComunicacionException
     */
    public Receptor(int puerto, TipoComunicacion tipoComunicacion) throws ComunicacionException {

        this.tipoComunicacion = tipoComunicacion;

        if (tipoComunicacion.equals(TipoComunicacion.TCP)) {
            String direccionIP = Utilidades.resolverDireccionLocal();
            if (direccionIP == null) {
                throw new ComunicacionException("La configuración del receptor no se pudo realizar");
            }

            InetAddress direccionLocal;
            try {
                direccionLocal = InetAddress.getByName(direccionIP);
                socketServidorTCP = new ServerSocket(puerto, MAXIMO_DE_CONEXIONES, direccionLocal);
            } catch (UnknownHostException ex) {
                throw new ComunicacionException("La configuración del receptor no se pudo realizar. No se cuenta con una dirección IP local válida", ex);
            } catch (IOException ex) {
                throw new ComunicacionException("La configuración del receptor no se pudo realizar", ex);
            }
        } else {
            try {
                socketMultiCast = new MulticastSocket(puerto);

                Properties prop = Utilidades.obtenerConfiguracion();
                String direccionIP = prop.getProperty(ConstantesConfiguracion.DIRECCION_IP_MULTICAST.getValor());

                grupo = InetAddress.getByName(direccionIP);
                socketMultiCast.joinGroup(grupo);
            } catch (UnknownHostException ex) {
                throw new ComunicacionException("La configuración del receptor no se pudo realizar. Configuración de Dirección IP Multicast errónea", ex);
            } catch (IOException ex) {
                throw new ComunicacionException("La configuración del receptor no se pudo realizar", ex);
            }
        }
    }

    /**
     * Método encargado de iniciar un hilo receptor de mensajes según la configuración inicial de
     * comunicación. Escucha mensajes de registro o eliminación de un cliente, o mensajes de
     * actualización de la frecuencia de toma de temperatura.
     */
    @Override
    public synchronized void run() {
        if (tipoComunicacion.equals(TipoComunicacion.TCP)) {
            while (true) {
                MensajeTCP recibido = new MensajeTCP();
                try {
                    //Se recibe un mensaje de un cliente de actualización de frecuencia.
                    socketClienteTCP = socketServidorTCP.accept();
                    ReceptorTCP receptorTCP = new ReceptorTCP(socketClienteTCP);
                    recibido = receptorTCP.recibir();
                    Logger.getLogger(Receptor.class.getName()).log(Level.INFO, "Actualizaci\u00f3n de temperatura por: {0} {1}°C", new Object[]{recibido.getNombreDireccion(), recibido.getFrecuenciaLectura()});

                } catch (IOException ex) {
                    Logger.getLogger(Receptor.class.getName()).log(Level.SEVERE, "No se pudo llevar a cabo la conexión entrante", ex);
                } catch (ComunicacionException ex) {
                    Logger.getLogger(Receptor.class.getName()).log(Level.SEVERE, "No se pudo llevar a cabo la conexión entrante", ex);
                }

                //Luego de leer quien hace la actualización, envía una notificación a todos los clientes del cambio de frecuencia utilizando el Almacén.
                Transmisor transmisor = new Transmisor();
                Map<String, UbicacionFisica> ubicaciones = Almacen.getInstancia().getUbicaciones();
                for (String clienteRemoto : ubicaciones.keySet()) {
                    try {
                        if (!clienteRemoto.equals(recibido.getNombreDireccion())) {//Evitar enviar mensaje al mismo que notificó
                            UbicacionFisica ubicacionRemota = ubicaciones.get(clienteRemoto);
                            transmisor.enviar(ubicacionRemota.getDireccionIP(), ubicacionRemota.getPuerto(), TipoComunicacion.TCP, recibido);
                            Logger.getLogger(Receptor.class.getName()).log(Level.SEVERE, "Se actualiz\u00f3: {0}", clienteRemoto);
                        }
                    } catch (ComunicacionException ex) {
                        Logger.getLogger(Receptor.class.getName()).log(Level.SEVERE, "No se pudo llevar a cabo la actualizaci\u00f3n de: {0}", clienteRemoto);
                    }
                }

            }
        } else {
            while (true) {
                ReceptorUDP receptorUDP = new ReceptorUDP(socketMultiCast);
                try {
                    //Se recibe un mensaje de un cliente que según su encabezado puede ser de registro o eliminación en el Almacén de direcciones.
                    MensajeUDP recibido = receptorUDP.recibir();
                    if (recibido.getEncabezado().equals(TiposEncabezados.REGISTRO.name())) {
                        Logger.getLogger(Receptor.class.getName()).log(Level.INFO, "Se agreg\u00f3: {0}", new Object[]{recibido.getNombreDireccion()});
                        Almacen.getInstancia().adicionarUbicacionRemota(recibido.getNombreDireccion(), recibido.getUbicacionFisica());
                    } else if (recibido.getEncabezado().equals(TiposEncabezados.ELIMINACION.name())) {
                        Logger.getLogger(Receptor.class.getName()).log(Level.INFO, "Se fue: {0}", new Object[]{recibido.getNombreDireccion()});
                        Almacen.getInstancia().eliminarUbicacion(recibido.getNombreDireccion());
                    }
                } catch (ComunicacionException ex) {
                    Logger.getLogger(Receptor.class.getName()).log(Level.SEVERE, "No se pudo llevar a cabo la recepción del mensaje", ex);
                }
            }
        }
    }
}
